特别策划 学习锦囊(三):听听课代表们怎么说

你好,我是课程编辑叶芊。

为了帮助你在新的一年更好地学习Rust和学习这个专栏,特地邀请了几位课代表来分享一下他们学习专栏、学习Rust语言的个人经验和方法,希望能给你一些参考和启发。

今天是特邀课代表分享个人学习经验的第三辑。话不多说,我们直接看分享。


@Milittle

你好,我是Milittle。

2020年硕士毕业,研究生期间一直是用C++做项目,目前我在做IaaS开发,主力语言是Python和Go。我今年的目标就是拿下Rust这门硬通货。非常开心收到编辑的邀请来分享我自己的学习心得。每一个人的经历不同,也有属于自己的人生,这里我就自己目前的一些感悟聊一聊。

为什么想到来学Rust

第一次了解Rust,是在左耳朵耗子的CoolShell中看到对这门语言的描述:

如果你对Rust的概念认识的不完整,你完全写不出程序,哪怕就是很简单的一段代码。这逼着程序员必须了解所有的概念才能编码。但是,另一方面也表明了这门语言并不适合初学者……

我看到这句话的时候,非常好奇这是一门什么样的语言,为什么会逼着程序员必须了解所有的概念才能编码?我们的编程语言不是为了解放程序员而发明的么?就去大致浏览了一些Rust Book的内容,不过当时刚工作也没有太深入地了解和学习。

因为我常年混迹在极客时间上学习,后来看到有陈天Rust第一课这门课程,刚好工作上每天也可以稍微抽出一些时间,就决定入手学一下。每次打开一讲,都是新知识涌现的过程。碰到了不熟悉的知识,我都会先自己借助Google一通操作,把能捕捉到的知识都先吃到自己的大脑里面,然后再回过头来看老师的专栏,这样能让我的知识叠的厚一点。

新知识确实很多,也很容易丧失学习的兴趣,被难题困住一阵就放弃了。我自己是用了两个方法:持续学习、重复练习

持续学习

在生活中,我自己是一个涉猎比较广的选手,自己学摄影,喜欢看电影,偶尔看看经济学的书籍,我觉得未来一定是留给持续学习、持续做准备的人。

所以相信自己,如果持续学习到今天,不能让你从Rust的难题中解脱出来,那就是你不够持续,还得再坚持下去不要放弃,说不定明天你就成功战胜了Rust难关。

而且持续学习,还可以用在那些不会立马给你回报的知识上,这样才能持续刺激自己学习的兴趣。

我在研究生期间有一个研究方向是借助机器学习视觉技术做一个基本的步态识别,虽然后来由于种种原因没有做下去,自己还是做了个小Demo,感兴趣可以到我的GitHub上瞧一瞧(步态识别案例)。因为一直做视觉,对计算机视觉中的一些inference的技术感兴趣,我自己结合Nvidia官网的TensorRT,业余时间自己持续学习也做了一些Demo(TensorRT的案例)。

因为有这个习惯,学习Rust的时候,我每天都在思考怎么用它做点事情。正好20211202这一天是回文日,就想到可以搞一个算法题,输出所有已经过去的回文日,第二天立马上手用Rust试了一下,就写了这一段代码:

use std::collections::HashMap;

fn main() {
    let mut valid_palindrome: Vec<String> = Vec::new();
    let mut month_days = HashMap::from([
        ("01", "31"),
        ("02", "28"),
        ("03", "31"),
        ("04", "30"),
        ("05", "31"),
        ("06", "30"),
        ("07", "31"),
        ("08", "31"),
        ("09", "30"),
        ("10", "31"),
        ("11", "30"),
        ("12", "31"),
    ]);

    for i in 1..=9999 {
        let full_year = format!("{:0width$}", i, width = 4).to_string();
        let month = &full_year[2..].chars().rev().collect::<String>();
        let day = &full_year[..2].chars().rev().collect::<String>();
        if is_leap_year(full_year.parse::<i32>().unwrap()) {
            let value = month_days.get_mut("02").unwrap();
            *value = "29";
        }
        if is_valid(month, day, &month_days) {
            let s = format!("{} {}-{}", full_year.parse::<i32>().unwrap(), month, day);
            valid_palindrome.push(s);
        }
        let value = month_days.get_mut("02").unwrap();
        *value = "28";
    }
    println!("{:?}", valid_palindrome);
}

#[allow(dead_code)]
fn is_valid(month: &str, day: &str, days: &HashMap<&str, &str>) -> bool {
    if month <= "12" && month != "00" {
        match days.get(month) {
            Some(&d) => {
                if day <= d && day != "00" {
                    return true;
                }
            }
            _ => return false,
        }
    }
    false
}

fn is_leap_year(year: i32) -> bool {
    if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
        return true;
    }
    false
}

你可以运行一下,会惊奇的发现,从公元0001年到公元9999年,回文日总共是366天,仔细一想其实是非常直观合理的,但是如果之前没有写过或者思考过,一定不能脱口而出。这只是一个简单的小例子,当持续学习的劲头保持下去,你会发现更多有趣的事情。

这些都是一些小小的Demo,但是在促使着我不断学习、提升自己。到现在,我的GitHub有很多自己写的一些示例,虽然每一个Demo不一定能为自己带来很多的回报,但是在让自己保持学习的过程中,我一直都会感觉到非常兴奋、非常愉快。

重复练习

持续学习是一个长久的过程,但是面对困难的单个知识点,就要靠重复练习了。

重复是所有人学习的最终杀器。这个心得来自于我的高中老师,他说如果你学不会,就重复多次,一遍不会就来两遍,两遍不会就四遍,重复多看几次一定能看明白。当然,不是所有知识都是能通过重复练习来理解或者获取到的,但我相信绝大多数的知识是可以通过重复训练掌握的。

学习Rust,重复练习更是我做的最多的一件事情。比如我读完Rust Book,会回过头来看老师的专栏,再去找bilibili的课程去重复巩固,学习其他人的观点和思路。举我学习异步编程的例子吧,重复练习才让我理解的更深刻。

这是学习Rust异步小册子的一段代码,我稍微修改了一下:

async fn learn_song() -> f64 {
    let mut sum: f64 = 0.0;
    for _i in 0..10 {
        tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
        sum += 1 as f64;
    }
    sum
}

async fn sing_song() {
    println!("sing song");
}

async fn dance() {
    tokio::time::sleep(tokio::time::Duration::from_nanos(2)).await;
    println!("dance");
}

async fn learn_and_sing_song() {
    let learned = learn_song();
    println!("i already learned song: {}", learned.await);
    sing_song().await;
}

async fn create_dir() {
    let beg = std::time::Instant::now();
    tokio::fs::create_dir("./test").await;
    println!("create folder consume: {:?}", beg.elapsed().as_micros());
}

async fn async_main() {
    let (_a, _b) = tokio::join!(dance(), create_dir());
}

#[tokio::main]
async fn main() {
    let now = tokio::time::Instant::now();
    async_main().await;
    println!("{:?}", now.elapsed().as_micros());
}

通过对代码重复地练习和思考,我总结出了一些自己容易理解的点:

  • async关键字,会把一个函数块或者代码块转换为一个Future,这个Future代表的是这个代码块想要运行的数据的步骤。
  • 当我们想运行一个Future的时候,需要使用关键字await,这个关键字会使得编译器在代码处用一个loop{}来运行该Future。
  • Future对象里面有poll方法,这个poll方法负责查看Future的状态机是否为Ready,如果不是Ready,则Pending;如果是Ready的话,就返回结果。如果为Pending的话,_task_context会通过yield,把该Future的线程交出去,让别的Future继续在这个线程上运行。
  • 因为Future的底层对象是由Generator构成的,所以调用poll方法的时候,其实调用的是genrator的resume方法,这个方法会把Generator的状态机(Yielded、Completed)返回给poll。如果为Yielded,poll返回Pending;否则Completed返回Ready,当返回Ready的时候,loop就被跳出,这个Future不会被再次执行了。

这些结论就是自己在重复探索的时候,不断尝试展并且展开编译器编译后的代码才发现的知识点,希望这些可以给你一些学习Rust的启发:

// $cargo rustc -r -- -Zunpretty=hir -o test.txt

// source code for learn_song
async fn learn_song() -> f64 {
    let mut sum: f64 = 0.0;
    for _i in 0..10 {
        tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
        sum += 1 as f64;
    }
    sum
}



// expand hir for learn_song
async fn learn_song()
 -> /*impl Trait*/ 
 #[lang = "from_generator"]
(
    move |mut _task_context| {
        {
            let _t = {
                    let mut sum: f64 =0.0;
                    {
                        let _t = match #[lang = "into_iter"](#[lang = "Range"]{start: 0, end: 10,}) {
                                mut iter => loop {
                                    match #[lang = "next"](&mut iter) {
                                        #[lang = "None"] {} => break,
                                        #[lang = "Some"] { 0: _i } => {
                                            match #[lang = "into_future"](tokio::time::sleep(tokio::time::Duration::from_millis(1))) {
                                                mut pinned => loop {
                                                    match unsafe {
                                                            #[lang = "poll"](#[lang = "new_unchecked"](&mut pinned),
                                                            #[lang = "get_context"](_task_context))
                                                        }
                                                        {
                                                            #[lang = "Ready"] { 0: result } => break result,
                                                            #[lang = "Pending"] { } => { }
                                                        }
                                                        _task_context = (yield ());
                                                },
                                            };
                                            sum += 1 as f64;
                                        }
                                    }
                                },
                            };
                        _t
                    };
                    sum
                };
            _t
        }
    }
)

其实在学习新知识的过程中,谁也不能一次性把知识点吃透,都是在不断重复之前的知识,然后加上自己的思考,在这个过程中不断汲取,内化为自己的知识。这样通过多次的打怪升级,最后我们也一定会有自己独到的见解。

学习资料

最后,也整理了一些我学习的相关资料分享给你,希望对你有所帮助:

寄语

可能你的学习方法和我的不一样,但是只要你选择了这门课程,就不要后悔,一如既往地保持自己学习Rust的初心,以持续学习的心态,拿出重复多次学习的决心来战胜Rust编程第一课,不要抱怨,不要后悔,勇往直前。你可以的。


这是今天课代表Milittle同学的分享,如果你有自己的Rust学习故事,欢迎在下方留言区留言交流。

预祝你新年快乐,身体健康,学习进步~